package com.citybox.infrastructure.wx.persistence.service.impl;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.citybox.domain.user.UserDo;
import com.citybox.domain.wx.*;
import com.citybox.infrastructure.user.persistence.service.RedisService;
import com.citybox.infrastructure.user.persistence.service.UserService;
import com.citybox.infrastructure.wx.persistence.service.WxService;
import com.citybox.shared.exception.Exp;
import com.citybox.shared.exception.enums.RCode;
import com.citybox.shared.utils.StringUtils;
import com.citybox.shared.utils.http.HttpUtils;
import com.citybox.shared.utils.jwt.JwtTokenUtil;
import com.citybox.shared.utils.jwt.WechatDecryptUtil;
import com.citybox.shared.web.domain.R;
import com.citybox.type.constant.WxConstant;
import com.citybox.type.enums.WxEnum;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;

import java.io.IOException;
import java.util.Map;
import java.util.Optional;

@Slf4j
@Service
public class WxServiceImpl implements WxService {

    @Value("${wx.APP_ID}")
    private String appId;

    @Value("${wx.APP_KEY}")
    private String secret;

    @Resource
    private WechatDecryptUtil wechatDecryptUtil;

    @Resource
    private UserService userService;

    @Resource
    private JwtTokenUtil jwtTokenUtil;

    @Resource
    private RedisService redisService;

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public R<WechatSessionDo> jsCode2Session(String code) {
        String requestUrl = String.format("%s?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
                WxConstant.LOGIN, appId, secret, code);


        try {
            String response = HttpUtils.doGet(requestUrl);
            log.debug("WeChat API response: {}", response);

            WechatSessionDo session = objectMapper.readValue(response, WechatSessionDo.class);

            if (!session.isSuccess()) {
                log.error("WeChat login failed: {} - {}", session.getErrcode(), session.getErrmsg());
                //throw new RuntimeException("微信登录失败: " + session.getErrmsg());
                return R.fail(session.getErrcode(),session.getErrmsg());
            }

            log.info("WeChat login success, openid: {}", session.getOpenid());
            return R.ok(session);
        } catch (Exception e) {
            log.error("Call WeChat API failed", e);
            throw new RuntimeException("调用微信接口失败: " + e.getMessage());
        }
    }

    @Override
    public boolean checkSignature(String rawData, String sessionKey, String signature) {
        try {
            log.info("Checking data signature");
            return wechatDecryptUtil.checkSignature(rawData, sessionKey, signature);
        } catch (Exception e) {
            log.error("Check signature failed", e);
            return false;
        }
    }


    @Transactional
    @Override
    public R<LoginResultDo> wechatLogin(LoginRequestDo request) {
        log.info("WeChat login request received, code: {}", request.getCode());

        R<WechatSessionDo> session = jsCode2Session(request.getCode());
        if(!session.isSuccess()){
            return R.fail(session.getCode(), session.getMsg());
        }
        if (request.getRawData() != null && request.getSignature() != null) {
            boolean validSignature = checkSignature(
                    request.getRawData(),
                    session.getData().getSessionKey(),
                    request.getSignature()
            );
            if (!validSignature) {
                return R.fail(RCode.API_CALL_FAILED.getCode(),"数据签名验证失败");
            }
        }

        UserDo user;
        if (request.getEncryptedData() != null && request.getIv() != null) {
            try {
                WechatUserInfoDo userInfo = decryptUserInfo(
                        request.getEncryptedData(),
                        session.getData().getSessionKey(),
                        request.getIv()
                );

                user = userService.findOrCreateUser(
                        session.getData().getOpenid(),
                        session.getData().getUnionid()
                );

                log.info("User info processed with decrypted data, nickname: {}", userInfo.getNickName());

            } catch (Exception e) {
                log.warn("User info decryption failed, creating user with basic info only: {}", e.getMessage());
                user = userService.findOrCreateUser(session.getData().getOpenid(), session.getData().getUnionid());
            }
        } else {
            user = userService.findOrCreateUser(session.getData().getOpenid(), session.getData().getUnionid());
        }

        String accessToken = jwtTokenUtil.generateAccessToken(user.getId(), user.getNickName());
        String refreshToken = jwtTokenUtil.generateRefreshToken(user.getId(), user.getNickName());

        String redisKey = "refresh_token:" + user.getId();
        redisService.set(
                redisKey,
                refreshToken,
                jwtTokenUtil.getRefreshTokenExpire()
        );

        log.info("Login success, userId: {}", user.getId());

        return R.ok(LoginResultDo.builder()
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .expiresIn(jwtTokenUtil.getAccessTokenExpire())
                .refreshExpiresIn(jwtTokenUtil.getRefreshTokenExpire())
                .tokenType("Bearer")
                .id(user.getId())
                .nickName(user.getNickName())
                .build());
    }

    @Override
    public R<TokenRefreshResultDo> refreshToken(TokenRefreshRequestDo request) {
        log.info("Token refresh request received");

        String refreshToken = request.getRefreshToken();

        if (!jwtTokenUtil.validateToken(refreshToken)) {
            log.error("Refresh token validation failed");
            return R.fail(RCode.PARAM_NOT_VALID.getCode(),"Refresh token 无效或已过期");
        }

        if (!"refresh".equals(jwtTokenUtil.getTokenType(refreshToken))) {
            log.error("Invalid token type for refresh");
            return R.fail(RCode.NOT_USER_ROLE_TYPE.getCode(),"Token 类型错误");
        }

        String userId = jwtTokenUtil.getUserIdFromToken(refreshToken);
        if (StringUtils.isEmpty(userId)) {
            log.error("Cannot get user id from refresh token");
            return R.fail(RCode.USERNAME_NOT_FOUND.getCode(),"无法从 token 中获取用户ID");
        }

        String redisKey = "refresh_token:" + userId;
        Object storedRefreshToken = redisService.get(redisKey);
        if (storedRefreshToken == null) {
            log.error("Refresh token is null for user: {}", userId);
            return R.fail(RCode.NOT_FOUND.getCode(),"Refresh token 为空");
        }
        if (!refreshToken.equals(storedRefreshToken)) {
            log.error("Refresh token mismatch for user: {}", userId);
            return R.fail(RCode.PARAM_NOT_VALID.getCode(),"Refresh token 不匹配");
        }

        UserDo user = userService.findById(userId);
        if (user == null) {
            log.error("User not found: {}", userId);
            return R.fail(RCode.USERNAME_NOT_FOUND.getCode(),"用户不存在");
        }

        String newAccessToken = jwtTokenUtil.generateAccessToken(user.getId(), user.getNickName());
        String newRefreshToken = jwtTokenUtil.generateRefreshToken(user.getId(), user.getNickName());

        redisService.set(redisKey, newRefreshToken, jwtTokenUtil.getRefreshTokenExpire());

        log.info("Token refresh success, userId: {}", userId);

        return R.ok(TokenRefreshResultDo.builder()
                .accessToken(newAccessToken)
                .refreshToken(newRefreshToken)
                .expiresIn(jwtTokenUtil.getAccessTokenExpire())
                .tokenType("Bearer")
                .build());
    }

    public R<WxAccessTokenDo> getAccessToken() {
        // 1. 定义常量键名
        final String tokenKey = WxEnum.ACCESS_TOKEN_KEY.getCode();
        final String tokenField = WxEnum.ACCESS_TOKEN.getCode();
        final String expiresInField = WxEnum.EXPIRES_IN.getCode();

        // 2. 尝试从缓存获取
        Object accessToken = redisService.get(tokenKey);
        long expiresIn;

        if (ObjectUtils.isEmpty(accessToken)) {
            // 3. 缓存不存在，请求微信接口
            try {
                String requestUrl = String.format("%sgrant_type=client_credential&appid=%s&secret=%s",
                        WxConstant.GET_ACCESS_TOKEN, appId, secret);

                String response = HttpUtils.doGet(requestUrl);
                if (ObjectUtils.isEmpty(response)) {
                    log.error("获取access_token失败，微信返回: {}", response);
                    return R.fail(RCode.NOT_RETURN_PARAMETER);
                }

                JSONObject jsonResponse = JSON.parseObject(response);
                accessToken = jsonResponse.getString(tokenField);

                if (ObjectUtils.isEmpty(accessToken)) {
                    log.error("获取access_token失败，微信返回: {}", response);
                    return R.fail(RCode.NOT_RETURN_PARAMETER.getCode(), response);
                }

                // 4. 设置缓存（提前200秒过期）
                expiresIn = jsonResponse.getLongValue(expiresInField);
                redisService.set(tokenKey, accessToken, expiresIn);

            } catch (Exception e) {
                log.error("获取access_token异常", e);
                return R.fail(RCode.SYSTEM_ERROR.getCode(), "获取access_token异常");
            }
        } else {
            // 5. 从缓存获取剩余时间
            expiresIn = redisService.getExpire(tokenKey);
        }
        WxAccessTokenDo wxAccessTokenDo = new WxAccessTokenDo();
        wxAccessTokenDo.setAccessToken(accessToken);
        wxAccessTokenDo.setExpiresIn(expiresIn);

        return R.ok(wxAccessTokenDo);
    }

    @Override
    public R<String> getUserPhoneNumber(String code) {
        // 1. 参数校验
        if (StringUtils.isEmpty(code)) {
            return R.fail(RCode.PARAM_NOT_VALID);
        }

        try {
            // 2. 获取access token
            R<WxAccessTokenDo> accessTokenDo = getAccessToken();
            if (!accessTokenDo.isSuccess()) {
                return R.fail(accessTokenDo.getCode(), accessTokenDo.getMsg());
            }

            // 3. 构建请求
            String url = WxConstant.GET_USER_PHONE_NUMBER + "access_token=" + accessTokenDo.getData().getAccessToken();
            Map<String, String> requestData = Map.of("code", code);

            // 4. 发送请求并处理响应
            String response = HttpUtils.doPost(url, JSON.toJSONString(requestData));
            JSONObject jsonResponse = JSON.parseObject(response);
            Integer errCode = jsonResponse.getInteger("errcode");
            if (errCode != null && errCode != 0) {
                log.error("获取用户手机号API失败：{}",jsonResponse);
                return R.fail(RCode.API_CALL_FAILED);
            }
            // 5. 解析手机号
            String phoneNumber = Optional.ofNullable(jsonResponse.getJSONObject("phone_info"))
                    .map(info -> info.getString("phoneNumber"))
                    .filter(StringUtils::isNotBlank)
                    .orElseThrow(() -> new Exp("手机号解析失败"));

            return R.ok(phoneNumber);

        } catch (IOException e) {
            log.error("获取用户手机号失败, code: {}", code, e);
            return R.fail(RCode.API_CALL_FAILED);
        } catch (Exception e) {
            log.error("解析用户手机号异常, code: {}", code, e);
            return R.fail(RCode.SYSTEM_ERROR);
        }
    }

    private WechatUserInfoDo decryptUserInfo(String encryptedData, String sessionKey, String iv) {
        try {
            log.info("Decrypting WeChat user info");

            String decryptedData = wechatDecryptUtil.decrypt(encryptedData, sessionKey, iv);
            log.debug("Decrypted data: {}", decryptedData);

            WechatUserInfoDo userInfo = objectMapper.readValue(decryptedData, WechatUserInfoDo.class);

            if (!userInfo.isValid(appId)) {
                log.error("Invalid watermark in decrypted data");
                throw new RuntimeException("解密数据水印验证失败");
            }

            log.info("WeChat user info decryption successful, nickname: {}", userInfo.getNickName());
            return userInfo;

        } catch (Exception e) {
            log.error("WeChat user info decryption failed", e);
            throw new RuntimeException("微信用户信息解密失败: " + e.getMessage());
        }
    }
}
